#include <QtXml>

#include "RoComponent.h"
#include "RoModule.h"
#include "RoStanzas.h"
#include "RoXmlStream.h"


RoXmlStream::RoXmlStream(RoAbstractModule *module, QIODevice *device, uint flags = 0):
    super(module),
    _device(device),
    reader(0),
    writer(0),
    isOutgoingStreamOpened(false),
    elementLevel(0),
    currentId(0),
    logIncomingTrafic(DontLog),
    logOutgoingTrafic(DontLog)
{
    if ( flags & LogIncomingTrafic )
        logIncomingTrafic = logTraficToQDebug;

    if ( flags & LogOutgoingTrafic )
        logOutgoingTrafic = logTraficToQDebug;
}


QIODevice * RoXmlStream::device() const
{
    return _device;
}


RoAbstractModule * RoXmlStream::parentModule() const
{
    return qobject_cast<RoAbstractModule*>(parent());
}


inline bool RoXmlStream::writeStanza(RoStanza stanza)
{
    if ( not writer or
         not writer->device()->isOpen() )
    {
        return false; // error
    }

    if ( stanza.id().isNull() ) // пересылаемым строфам id не присваивается
        stanza.documentElement().setAttribute("id", nextId().toString());

    logOutgoingTrafic("OUT", stanza);
    writeDomElement(stanza.documentElement());

    return true; // success
}


RoStanzaId RoXmlStream::nextId()
{
    return QString::number( ++currentId );
}


bool RoXmlStream::isOpen()
{
    return isOutgoingStreamOpened and
           reader->device()->isReadable() and
           writer->device()->isWritable();
}


void RoXmlStream::openStream()
{
    // Открытие XML-потока в соответствии со спеками

    if ( not isOutgoingStreamOpened )
    {
        connect(_device, SIGNAL( readyRead() ),
                this, SLOT( read() ));

        reader = new QXmlStreamReader(_device);

        writer = new QXmlStreamWriter(_device);
        writer->setAutoFormatting(true);

        writer->writeStartDocument();
        writer->writeStartElement("stream");
        writer->writeAttribute("version", QString("%1.%2").arg(ROMASHKA_VERSION_MAJOR).arg(ROMASHKA_VERSION_MINOR));

        isOutgoingStreamOpened = true;
    }
}


void RoXmlStream::closeStream()
{
    // Закрытие XML-потока в соответствии со спеками

    elementLevel = 0; // на случай повторного открытия потока
    delete reader;
    reader = 0;

    if ( writer )
    {
        // Надюсь, мы закрываем <stream>
        // Способов проверить это я не нашёл, остаётся только соблюдать парность writeStartElement() + writeEndElement()
        writer->writeEndElement();
        writer->writeEndDocument();
        delete writer;
        writer = 0;
    }

    if ( _device )
    {
        _device->close();
    }

    isOutgoingStreamOpened = false;
}


inline RoAbstractModule * RoXmlStream::parent() const
{
    return qobject_cast<RoAbstractModule*>(super::parent());
}


void RoXmlStream::DomBuilder::appendTokenFromStream(QXmlStreamReader * reader)
{
    switch ( reader->tokenType() )
    {
    case QXmlStreamReader::Comment:
    case QXmlStreamReader::DTD:
    case QXmlStreamReader::EndDocument:
    case QXmlStreamReader::EntityReference:
    case QXmlStreamReader::Invalid:
    case QXmlStreamReader::NoToken:
    case QXmlStreamReader::ProcessingInstruction:
    case QXmlStreamReader::StartDocument:
        // ignore
        break;

    case QXmlStreamReader::StartElement:
        {
            QDomElement e = createElement(reader->name().toString());
            foreach ( QXmlStreamAttribute a, reader->attributes() )
                e.setAttribute(a.qualifiedName().toString(), a.value().toString());
            (currentNode.isNull() ? *this : currentNode).appendChild(e);
            currentNode = e;
        }
        break;

    case QXmlStreamReader::Characters:
        if ( not reader->isWhitespace() )
        {
            if ( reader->isCDATA() )
                currentNode.appendChild(createCDATASection(reader->text().toString()));
            else // simple text
                currentNode.appendChild(createTextNode(reader->text().toString()));
        }
        break;

    case QXmlStreamReader::EndElement:
        currentNode = currentNode.parentNode();
        break;
    }
}


void RoXmlStream::writeDomElement(QDomElement element)
{
    writer->writeStartElement(element.tagName());
    {
        // attributes
        QString xmlns = element.namespaceURI();
        if ( not xmlns.isEmpty() )
            writer->writeAttribute("xmlns", xmlns);
        QDomNamedNodeMap attrs = element.attributes();
        for ( int i = 0; i < attrs.size(); ++i )
        {
            QDomAttr attr = attrs.item(i).toAttr();
            writer->writeAttribute(attr.name(), attr.value());
        }

        // children
        for ( QDomNode childNode = element.firstChild(); not childNode.isNull(); childNode = childNode.nextSibling() )
        {
            switch ( childNode.nodeType() )
            {
            case QDomNode::ElementNode:
                writeDomElement(childNode.toElement());
                break;

            case QDomNode::TextNode:
                writer->writeCharacters(childNode.toText().data());
                break;

            case QDomNode::CDATASectionNode:
                writer->writeCDATA(childNode.toCDATASection().data());
                break;

            default:
                ; // ignore, nothing important
            }
        }
    }
    writer->writeEndElement();
}


inline QString RoXmlStream::localModuleName()
{
    return parent()->name();
}


inline QString RoXmlStream::remoteModuleName()
{
    QString n = "REMOTE_MODULE";

    QPointer<QTcpSocket> socket = qobject_cast<QTcpSocket*>(writer->device());
    if ( socket )
        n += "_" + socket->peerAddress().toString() + ":" + QString::number(socket->peerPort());

    return n;
}


inline void RoXmlStream::logTraficToQDebug(const QString & prefix, const RoStanza & stanza)
{
    qDebug() << static_cast<const char*>((prefix + ":\n" + stanza.toString(4)).toUtf8());
}


inline void RoXmlStream::DontLog(const QString &, const RoStanza &)
{
    // do nothing
}


// slot for _device->readyRead()
void RoXmlStream::read()
{
    // this function's skeleton was taken from QXmlStreamReader docs

    while ( not reader->atEnd() )
    {
        Q_ASSERT( elementLevel >= 0 );

        QXmlStreamReader::TokenType tokenType = reader->readNext();

        // вездесущие и просто игнорируемые токены
        if ( tokenType == QXmlStreamReader::Invalid or
             reader->isStartDocument() )
        {
            continue;
        }
        else if ( reader->isCharacters() and
                  reader->isWhitespace() )
        {
            continue;
        }

        // токены, значение которых зависит от положения
        if ( elementLevel == 0 ) // outside of the stream
        {
            // here only stream opening may be
            if ( reader->isStartElement() )
            {
                QString tagName = reader->name().toString();
                if ( tagName == "stream" )
                {
                    // stream opening
                    QXmlStreamAttributes attrs = reader->attributes();
                    if ( attrs.hasAttribute("version") )
                    {
                        QStringList versionStrings = attrs.value("version").toString().split('.');
                        int major = ROMASHKA_VERSION_MAJOR, minor = ROMASHKA_VERSION_MINOR;
                        if ( versionStrings.size() > 0 )
                            major = versionStrings[0].toUInt();
                        if ( versionStrings.size() > 1 )
                            minor = versionStrings[1].toUInt();
                        if ( major > ROMASHKA_VERSION_MAJOR or
                             ( major == ROMASHKA_VERSION_MAJOR and
                               minor > ROMASHKA_VERSION_MINOR ) )
                        {
                            writeStanza( RoErrorStanza(RoErrorStanza::UnsupportedVersion)
                                         .setAttribute("to", incomingStanza.from())
                                         .setAttribute("from", localModuleName()) );
                            closeStream();
                        }
                    }

                    // stream is open
                    ++elementLevel;
                }
                else
                {
                    writeStanza( RoErrorStanza(RoErrorStanza::NotAuthorized)
                                 .setAttribute("to", incomingStanza.from())
                                 .setAttribute("from", localModuleName()) );
                    closeStream();
                }
            }
        }
        else // elementLevel >= 1 // in the stream
        {
            // elementLevel == 1 — here only stanzas may be
            // elementLevel >= 2 — in a stanza

            if ( reader->isStartElement() )
            {
                ++elementLevel;

                if ( elementLevel == 2 )
                {
                    // let's begin building a stanza
                    incomingStanza = DomBuilder();
                }
            }

            incomingStanza.appendTokenFromStream(reader);

            if ( reader->isEndElement() )
            {
                --elementLevel;

                if ( elementLevel == 1 )
                {
                    // it's time to end building a stanza
                    logIncomingTrafic("IN", incomingStanza);

                    emit stanza(incomingStanza);
                }
            }
        }
    }
    if ( reader->hasError() )
    {
        switch ( reader->error() )
        {
        case QXmlStreamReader::PrematureEndOfDocumentError:
            break; // just wait for more data

        case QXmlStreamReader::NotWellFormedError:
            writeStanza( RoErrorStanza(RoErrorStanza::XmlNotWellFormed)
                         .setAttribute("to", remoteModuleName())
                         .setAttribute("from", localModuleName()) );
            closeStream();
            break;

        default: // some other error
            qDebug() << reader->error() << reader->errorString();
            Q_ASSERT(false); /// @todo
            break;
        }
    }
}
